using System;
using System.Collections.Generic;
using System.Text;
using TTDotNet.Util;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;

using Microsoft.Samples.DirectX.UtilityToolkit;

namespace TTDotNet.Engine.ThreeDEngine
{
    public class AJMesh : AJObject
    {
        private Material[] _meshMaterials;
        private Texture[] _meshTextures;
        private Mesh _mesh;

        private Random rng = new Random();

        private string _meshFile;
        
        private Vector3 low;
        private Vector3 high;

        private VertexDeclaration instanceDeclaration;
        private int batchSize = 240;

        private int numInstances = 0;

        // array for holding our instance specific data, which will be translation in xyz
        private Vector4[] instanceData;
        private Dictionary<Guid, int> lookup;
        private Dictionary<Guid, Vector3> positionData;
        private Dictionary<Guid, Vector3> rotationData;

        private ObjectType objectType;

        // needed to cut instance data into batchSize chunks, shouldn't be needed but lacking MDX docs... again
        private Vector4[] batchInstanceData;

        private Effect effect = null;

        // handles into the instancing shader
        private EffectHandle worldViewProjHandle, instanceDataHandle, tex0Handle;

        // batch sized buffers
        private IndexBuffer[] batchIndexBuffer;
        private VertexBuffer[] batchVertexBuffer;

        private AttributeRange[] range;
        //private Texture zomgzred;

        public AJMesh(string meshPath, ObjectType type)
        {
            positionData = new Dictionary<Guid, Vector3>();
            rotationData = new Dictionary<Guid, Vector3>();
            SetupEffect();
            SetupMesh(Utility.FindMediaFile(meshPath));
            objectType = type;
            Game.gameWorld.Scene.AddObject(this);  
        }

        private void SetupEffect()
        {
            ShaderFlags shaderFlags = ShaderFlags.None;
            string path = "data/ManagedInstancing.fx";
            effect = ResourceCache.GetGlobalInstance().CreateEffectFromFile(Game.device,
                Utility.FindMediaFile(path), null, null, shaderFlags, null);

            instanceDataHandle = effect.GetParameter(null, "InstanceData");
            worldViewProjHandle = effect.GetParameter(null, "WorldViewProjection");
            tex0Handle = effect.GetParameter(null, "Tex0");
        }

        private void SetupMesh(string path)
        {
            _meshFile = path;
            LoadResources();
            ComputeBB();

            batchInstanceData = new Vector4[batchSize];
            instanceData = new Vector4[batchSize];
            lookup = new Dictionary<Guid, int>();
        }

        protected override void ComputeRadius()
        {
            using (VertexBuffer vertexBuffer = _mesh.VertexBuffer)
            {
                GraphicsStream gStream = vertexBuffer.Lock(0, 0, LockFlags.None);
                Vector3 tempCenter;

                radius = Geometry.ComputeBoundingSphere(gStream, _mesh.NumberVertices, _mesh.VertexFormat, out tempCenter);

                vertexBuffer.Unlock();
            }
        }

        public override void SetScaling(Vector3 newScale)
        {
            base.SetScaling(newScale);
            mySize = new Vector3(GetWidth(), GetHeight(), GetLength());
        }

        public override void ScaleBy(Vector3 scaleFactor)
        {
            base.ScaleBy(scaleFactor);
            mySize = new Vector3(GetWidth(), GetHeight(), GetLength());
        }

        private void ComputeBB()
        {
            GraphicsStream vertexData = _mesh.VertexBuffer.Lock(0, 0, LockFlags.ReadOnly);
            Geometry.ComputeBoundingBox(vertexData, _mesh.NumberVertices, _mesh.VertexBuffer.Description.VertexFormat, out low, out high);
            _mesh.VertexBuffer.Unlock();
        }

        public float GetWidth()
        {
            return Math.Abs(low.X - high.X) * myScaling.X;
        }

        public float GetHeight()
        
        {
            return Math.Abs(low.Y - high.Y) * myScaling.Y;
        }

        public float GetLength()
        {
            return Math.Abs(low.Z - high.Z) * myScaling.Z;
        }

        public override void ReloadResources()
        {
            LoadResources();
            ReloadRotationData();
        }

        private void ReloadRotationData()
        {
            Guid[] guids = new Guid[rotationData.Keys.Count];
            rotationData.Keys.CopyTo(guids, 0);

            foreach (Guid guid in guids)
            {
                Vector3 v = new Vector3(rotationData[guid].X, rotationData[guid].Y, rotationData[guid].Z);
                rotationData[guid] = new Vector3();
                RotateInstanceTo(v, guid);
            }
        }

        private void LoadResources()
        {
            ExtendedMaterial[] materials = null;

            // Turn on the zbuffer
            Game.device.RenderState.ZBufferEnable = true;

            // Turn on ambient lighting 
            //Game.device.RenderState.Ambient = System.Drawing.Color.White;

            try
            {
                _mesh = Mesh.FromFile(_meshFile, MeshFlags.Use32Bit, Game.device, out materials);
            }
            catch (Exception e) { 
                System.Windows.Forms.MessageBox.Show("Error Loading mesh: \n " + e.Message); 
            }


            if (_meshTextures == null)
            {
                // We need to extract the material properties and texture names 
                _meshTextures = new Texture[materials.Length];
                _meshMaterials = new Material[materials.Length];
                for (int i = 0; i < materials.Length; i++)
                {
                    _meshMaterials[i] = materials[i].Material3D;
                    // Set the ambient color for the material (D3DX does not do this)
                    _meshMaterials[i].Ambient = _meshMaterials[i].Diffuse;

                    // Create the texture

                    if (materials[i].TextureFilename != null && (materials[i].TextureFilename != string.Empty))
                        _meshTextures[i] = TextureLoader.FromFile(Game.device, Utility.FindMediaFile(materials[i].TextureFilename));
                }
            }

            CreateBuffers();
        }

        public override void Render(AJCamera camera)
        {
            InstancedRender(camera);
        }

        private void InstancedRender(AJCamera camera)
        {
            Game.device.RenderState.Lighting = false;
            Game.device.RenderState.CullMode = Cull.None;

            effect.SetValue(worldViewProjHandle, Matrix.Identity * camera.View * camera.Projection);

            int fullBatches = numInstances / batchSize;

            Game.device.VertexDeclaration = instanceDeclaration;

            for (int batch = 0; batch < fullBatches; batch++)
            {
                Array.Copy(instanceData, batch * batchSize, batchInstanceData, 0, batchSize);
                effect.SetValue(instanceDataHandle, batchInstanceData);

                for (int att = 0; att < range.Length; att++)
                {
                    Game.device.SetStreamSource(0, batchVertexBuffer[att], 0);
                    Game.device.Indices = batchIndexBuffer[att];
                    effect.SetValue(tex0Handle, _meshTextures[att]);

                    int numPasses = effect.Begin(FX.None);

                    for (int pass = 0; pass < numPasses; pass++)
                    {
                        effect.BeginPass(pass);
                        Game.device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, range[att].VertexCount * batchSize, 0, range[att].FaceCount * batchSize);
                        effect.EndPass();
                    }

                    effect.End();

                }
            }

            int lastBatchSize = numInstances - (fullBatches * batchSize);
            Array.Copy(instanceData, fullBatches * batchSize, batchInstanceData, 0, lastBatchSize);

            effect.SetValue(instanceDataHandle, batchInstanceData);

            for (int att = 0; att < range.Length; att++)
            {
                Game.device.SetStreamSource(0, batchVertexBuffer[att], 0);
                Game.device.Indices = batchIndexBuffer[att];
                effect.SetValue(tex0Handle, _meshTextures[att]);

                int finalNumPasses = effect.Begin(FX.None);

                for (int pass = 0; pass < finalNumPasses; pass++)
                {
                    effect.BeginPass(pass);
                    Game.device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, range[att].VertexCount * batchSize, 0, range[att].FaceCount * lastBatchSize);
                    effect.EndPass();
                }

                effect.End();
            }

        }

        private void CreateBuffers()
        {
            range = _mesh.GetAttributeTable();

            if (range.Length > 0)
            {

                VertexElement[] decl = ShaderInstancingVertex.Declaration;
                instanceDeclaration = new VertexDeclaration(Game.device, decl);

                int instanceVertex = 0;
                int instanceIndex = 0;

                batchIndexBuffer = new IndexBuffer[range.Length];
                batchVertexBuffer = new VertexBuffer[range.Length];

                int attID = 0;

                int indexOffset = 0;

                foreach (AttributeRange ar in range)
                {
                    //Lets starts with the vertices.

                    batchIndexBuffer[attID] = new IndexBuffer(typeof(int), batchSize * ar.FaceCount * 3, Game.device, Usage.WriteOnly, Pool.Default);
                    batchVertexBuffer[attID] = new VertexBuffer(typeof(ShaderInstancingVertex), batchSize * ar.VertexCount, Game.device, Usage.Dynamic, ShaderInstancingVertex.Format, Pool.Default);

                    GraphicsStream gs = _mesh.LockVertexBuffer(LockFlags.None);
                    ShaderInstancingVertex[] instanceVertices = new ShaderInstancingVertex[batchSize * ar.VertexCount];

                    for (int instance = 0; instance < batchSize; instance++)
                    {
                        gs.Position = ar.VertexStart * _mesh.NumberBytesPerVertex;
                        for (int vertex = 0; vertex < ar.VertexCount; vertex++)
                        {
                            instanceVertex = instance * ar.VertexCount + vertex;
                            CustomVertex.PositionNormalTextured siv = (CustomVertex.PositionNormalTextured)gs.Read(typeof(CustomVertex.PositionNormalTextured));
                            gs.Position += ((gs.Length / _mesh.NumberVertices) - CustomVertex.PositionNormalTextured.StrideSize);

                            instanceVertices[instanceVertex] = new ShaderInstancingVertex(siv, new Vector4(instance, 0, 0, 0));
                        }
                    }

                    batchVertexBuffer[attID].SetData(instanceVertices, 0, LockFlags.None);
                    batchVertexBuffer[attID].Unlock();
                    _mesh.UnlockVertexBuffer();


                    //Now move onto the indices.

                    int[] meshIndices = (int[])_mesh.IndexBuffer.Lock(0, typeof(int), LockFlags.ReadOnly, ar.FaceCount * 3);
                    int[] instanceIndices = new int[batchSize * ar.FaceCount * 3];

                    // copy indices from original buffer, notice the shift to make the indices point to different vertices for each instance
                    for (int instance = 0; instance < batchSize; instance++)
                    {
                        for (int index = indexOffset; index < indexOffset + (ar.FaceCount * 3); index++)
                        {
                            instanceIndex = (instance * (ar.FaceCount * 3)) + (index - indexOffset);
                            instanceIndices[instanceIndex] = (int)(meshIndices[index - indexOffset] + (instance * ar.VertexCount));
                        }
                    }   

                    indexOffset += (ar.FaceCount * 3);

                    batchIndexBuffer[attID].SetData(instanceIndices, 0, LockFlags.None);
                    batchIndexBuffer[attID].Unlock();
                    _mesh.IndexBuffer.Unlock();

                    attID++;
                }
            }
        }

        public Guid AddNewInstance(Vector3 position)
        {
            Guid newGuid = Guid.Empty;

            if (numInstances < batchSize)
            {
                instanceData[numInstances] = new Vector4(position.X, position.Y, position.Z, 0);

                byte[] bytes = new byte[16];
                rng.NextBytes(bytes);

                newGuid = new Guid(bytes);
                //positionData[newGuid] = position;
                //rotationData[newGuid] = new Vector3();
                lookup[newGuid] = numInstances;
                numInstances++;
            }

            return newGuid;
        }

        public void MoveInstance(Vector3 position, Guid instance)
        {
            if (lookup.ContainsKey(instance))
            {
                positionData[instance] = position;
                instanceData[lookup[instance]] = new Vector4(position.X, position.Y, position.Z, 0);
            }
            else
            {
                throw new Exception("This instance does not exist!\n" + instance + " is not in the lookup table.");
            }
        }

        public void RotateInstanceTo(Vector3 rotation, Guid instance)
        {
            float xchange = rotation.X - rotationData[instance].X;
            float ychange = rotation.Y - rotationData[instance].Y;
            float zchange = rotation.Z - rotationData[instance].Z;

            if (xchange != 0 || ychange != 0 || zchange != 0)
            {
                rotationData[instance] = rotation;

                int vSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(ShaderInstancingVertex));

                for (int att = 0; att < range.Length; att++)
                {
                    int toRotate = range[att].VertexCount;
                    int startIndex = range[att].VertexCount * lookup[instance];

                    GraphicsStream gs = batchVertexBuffer[att].Lock((startIndex * vSize), (toRotate * vSize), LockFlags.None);

                    for (int i = 0; i < toRotate; i++)
                    {
                        ShaderInstancingVertex siv = (ShaderInstancingVertex)gs.Read(typeof(ShaderInstancingVertex));

                        Vector4 pos = new Vector4(siv.Position.X, siv.Position.Y, siv.Position.Z, 0);
                        pos = Vector4.Transform(pos, Matrix.RotationX(Geometry.DegreeToRadian(xchange)));
                        pos = Vector4.Transform(pos, Matrix.RotationY(Geometry.DegreeToRadian(ychange)));
                        pos = Vector4.Transform(pos, Matrix.RotationZ(Geometry.DegreeToRadian(zchange)));

                        gs.Position -= vSize;
                        Vector3 pos3 = new Vector3(pos.X, pos.Y, pos.Z);
                        gs.Write(pos3);
                        gs.Position += vSize - System.Runtime.InteropServices.Marshal.SizeOf(typeof(Vector3));
                    }

                    batchVertexBuffer[att].Unlock();

                }
            }
        }

        public void RemoveInstance(Guid instance)
        {
            if (lookup.ContainsKey(instance))
            {
                int index = lookup[instance];

                for (int i = index; i < instanceData.Length - 1; i++)
                {
                    Vector4 next = instanceData[i + 1];
                    instanceData[i] = new Vector4(next.X, next.Y, next.Z, next.W);
                }

                lookup.Remove(instance);

                numInstances--;
            }
        }

        public override PickData Pick(Vector3 rayStart, Vector3 rayDirection, AJCamera camera, out IntersectInformation intInf)
        {
            Guid[] keys = new Guid[positionData.Keys.Count];
            positionData.Keys.CopyTo(keys, 0);

            bool check = false;
            intInf = new IntersectInformation();

            // FIX TO FIND THE NEAREST!!!1
            foreach (Guid guid in keys)
            {
                // Convert ray to model space
                Matrix World =
                    Matrix.RotationYawPitchRoll(Geometry.DegreeToRadian(rotationData[guid].Y), Geometry.DegreeToRadian(rotationData[guid].X), 
                                                    Geometry.DegreeToRadian(rotationData[guid].Z)) *
                    Matrix.Translation(positionData[guid]);

                Matrix inverseWorld = Matrix.Invert(World);

                Vector3 localStart = Vector3.TransformCoordinate(rayStart, inverseWorld);
                Vector3 localDirection = Vector3.TransformNormal(rayDirection, inverseWorld);
                localDirection.Normalize();

                // Check for mesh intersection
                check =  _mesh.Intersect(localStart, localDirection, out intInf);
                if (check)
                {
                    return new PickData(guid, true);
                }
            }

            return new PickData(Guid.Empty, false);

        }

        public ObjectType ObjectType
        {
            get { return objectType; }
        }

    }

    public struct ShaderInstancingVertex
    {
        public Vector3 Position;
        public Vector3 Normal;
        public float Tu;
        public float Tv;
        public Vector4 Instance;

        public ShaderInstancingVertex(CustomVertex.PositionNormalTextured vertex, Vector4 instance)
        {
            this.Position = vertex.Position;
            this.Normal = vertex.Normal;
            this.Tu = vertex.Tu;
            this.Tv = vertex.Tv;
            this.Instance = instance;
        }

        public static readonly VertexFormats Format = VertexFormats.Position | VertexFormats.Normal | VertexFormats.Texture0 | VertexFormats.Texture1;
        public static readonly VertexElement[] Declaration = new VertexElement[]
		{
			new VertexElement(0, 0, DeclarationType.Float3, DeclarationMethod.Default, DeclarationUsage.Position, 0),
			new VertexElement(0, 12, DeclarationType.Float3, DeclarationMethod.Default, DeclarationUsage.Normal, 0),
			new VertexElement(0, 24, DeclarationType.Float2, DeclarationMethod.Default, DeclarationUsage.TextureCoordinate, 0),
            new VertexElement(0, 32, DeclarationType.Float4, DeclarationMethod.Default, DeclarationUsage.TextureCoordinate, 1),
			VertexElement.VertexDeclarationEnd
		};

        public override string ToString()
        {
            return "(" + Position.X + "," + Position.Y + "," + Position.Z + ")";
        }
    }

    

}
